Sintassi per testbench
Le testbench in Verilog sono moduli utilizzati per simulare e testare il comportamento di reti digitali (il "device under test" o DUT).
A differenza delle reti sintetizzabili, le testbench utilizzano costrutti non sintetizzabili come initial, $display, $finish, ecc., che servono solo per la simulazione.
In questa pagina documentiamo le principali sintassi e strutture utilizzate nelle testbench, con particolare attenzione a quelle comuni negli esercizi d'esame.
Non è necessario saper scrivere testbench, ma è utile saperle leggere per capire cosa succede durante una simulazione e debuggare in modo più efficace.
Struttura base di una testbench
Una testbench tipica è un modulo senza porte (input/output), che istanzia il DUT e genera stimoli per testarlo.
module testbench();
// Dichiarazioni di segnali (reg per input, wire per output)
reg input_signal;
wire output_signal;
// Istanziazione del DUT
DUT dut (
.input_signal(input_signal),
.output_signal(output_signal)
);
// Blocco initial per stimoli
initial begin
// Genera waveform
$dumpfile("waveform.vcd");
$dumpvars;
// Stimoli
input_signal = 0;
#10; // Aspetta 10 unità di tempo
input_signal = 1;
#10;
// Fine simulazione
$finish;
end
endmodule
La keyword initial introduce un blocco eseguito a partire dal tempo 0 della simulazione.
I comandi $dumpfile e $dumpvars servono a generare il file VCD da leggere con GTKWave.
Il comando $finish termina la simulazione.
Generatore di clock
Per reti sequenziali, è necessario un segnale di clock.
Si usa un modulo separato clock_generator:
module clock_generator(
clock
);
output clock;
parameter HALF_PERIOD = 5;
reg CLOCK;
assign clock = CLOCK;
initial CLOCK <= 0;
always #HALF_PERIOD CLOCK <= ~CLOCK;
endmodule
Nell'istanziazione:
wire clock;
clock_generator clk(.clock(clock));
Test di input-output
Usiamo if per verificare output e $display per stampare eventuali errori.
if (output_signal !== expected) begin
$display("Wrong output: expected %d, got %d instead", expected, output_signal);
end
Notare che non c'è alcuna stampa se l'output è corretto, quindi una simulazione che non stampa nulla è una prima indicazione (ma mai garanzia) di aver implementato correttamente la rete richiesta.
Per testare più casi, usiamo cicli for e funzioni automatic per generare testcase.
function automatic [15:0] get_testcase;
input [7:0] i;
reg [7:0] in;
reg [7:0] out;
begin
// Logica per generare {in, out} basato su i
in = i * 10;
out = in * 2;
get_testcase = {in, out};
end
endfunction
initial begin
...
reg [7:0] i;
reg [7:0] test_in;
reg [7:0] expected_out;
for (i = 0; i < 64; i++) begin
{test_in, expected_out} = get_testcase(i);
// Stimolo test_in a dut; controllo output
end
...
end
Linee di errore
Per evidenziare errori nelle waveform, usiamo variabili come reg error e un blocco always.
reg error;
// Reset di error a inizio simulazione
initial error = 0;
// Ogni volta che error va a 1, lo resettiamo poco dopo
always @(posedge error) #1 error = 0;
initial begin
...
if (condizione_errore) begin
$display("Errore rilevato");
error = 1;
end
...
end
Così facendo, nella waveform la linea error ci indica con degli impulsi facili da notare i punti della simulazione in cui sono stati rilevati errori.
Strutture avanzate: concorrenza e timeout
Quando la rete DUT deve interagire con più dispositivi indipendenti è necessario simulare questi in modo parallelo, per non introdurre temporizzazioni forzate.
L'esempio più comune sono produttori e consumatori, ciascuno con i propri handshake da tenere indipendenti dagli altri.
Usiamo per questo fork...join.
fork
begin : dispositivo_A
// Logica che simula la rete A
// Eventuale verifica degli output da DUT ad A
end
begin : dispositivo_B
// Logica che simula la rete B
// Eventuale verifica degli output da DUT a B
end
join
Un blocco fork...join esegue finché non sono terminati tutti i suoi sottoprocessi, oppure viene terminato anticipatamente con disable.
Sempre legato all'handshake c'è il problema del timeout: una rete DUT che non esegue correttamente i propri handshake porta a una simulazione che si blocca su attese infinite.
Utilizziamo quindi fork...join anche per introdurre un limite massimo di esecuzione per terminare le simulazioni evidentemente troppo lunghe.
initial begin
...
fork : f
begin
#100000; // Attesa molto molto lunga
$display("Timeout - waiting for signal failed");
disable f; // termina tutto il fork, procedendo alla $finish
end
begin
// Processo principale, con eventuali fork...join per dispositivi
end
join
$finish;
end